Est. Street

关于我的小笔记本熄半屏自动默哀这档子事(二)

Elvin Starry
Elvin Starry

0x00 前情提要

上回书说到,我的笔记本在Linux系统下当亮度降低越过50%这个值时屏幕会黑掉。当时留下了一个疑问:为什么亮度在50%时BRIL寄存器的值恒为0?
最近得闲,继续追查这个问题。第一部分排查至此还剩下两个可能导致BRIL=0的因素:amdgpu驱动问题,或者其他软件组件/硬件本身问题。
mmiotrace可以记录硬件驱动等对内存映射I/O(MMIO)的访问。我们可以用它来追踪软件层面对BRIL寄存器的修改。

0x01

首先检查一下Linux内核是否支持mmiotrace

sudo zgrep '^CONFIG_MMIOTRACE=' /proc/config.gz
# CONFIG_MMIOTRACE=y

支持的,可以准备追踪了。
mmiotrace只能记录启用之后经ioremap()创建的映射,所以需要在amdgpu建立MMIO映射前开始追踪。重启进grub,先把amdgpu关掉。内核命令行加:

systemd.unit=multi-user.target modprobe.blacklist=amdgpu

F10启动进入TTY,登录root账户。lsmod | grep amdgpu应当没有输出。
然后根据上一篇文章的伪代码:

GPUB = pci_config_read32(gpu, 0x24);     // PCI BAR5
base = GPUB & 0xFFFFFF00;
addr = base + 0x0138;
BRIL = read8(addr + 0x01);
     = (read32(base + 0x0138) >> 8) & 0xff;

计算一下BRIL的物理地址:

BDF=0000:03:00.0

read BAR5_START BAR5_END BAR5_FLAGS < <(
    sed -n '6p' /sys/bus/pci/devices/$BDF/resource
)

REG138=$((BAR5_START + 0x138))
BRIL_ADDR=$((BAR5_START + 0x139))

printf 'BAR5 = 0x%x\n' "$BAR5_START"
printf 'REG138 = 0x%x\n' "$REG138"
printf 'BRIL = 0x%x\n' "$BRIL_ADDR"

# BAR5 = 0xfd400000
# REG138 = 0xfd400138
# BRIL = 0xfd400139

然后新建一个shell脚本。开启mmiotrace后它会把除了CPU0以外的所有核心全部下线,系统反应会比较迟钝,键盘上实时输入可能反应比较慢。

# stage1.sh

OUT=/var/tmp/mmiotrace.log
mount -t tracefs nodev /sys/kernel/tracing
# GPU MMIO 流量很大,给一个大的缓冲区
echo 65536 > /sys/kernel/tracing/buffer_size_kb
# 选择 mmiotrace
echo mmiotrace > /sys/kernel/tracing/current_tracer
# 持续消费日志
cat /sys/kernel/tracing/trace_pipe > $OUT &
CATPID=$!

echo 1 > /sys/kernel/tracing/tracing_on
echo "log: $OUT"
echo "reader PID: $CATPID"

# 然后加载amdgpu
modprobe amdgpu
# 驱动与背光设备出现
lsmod | grep amdgpu
ls /sys/class/backlight/
# stage2.sh

MAX=$(cat /sys/class/backlight/amdgpu_bl1/max_brightness)
HALF=$(((MAX + 1) / 2))

echo $HALF > /sys/class/backlight/amdgpu_bl1/brightness

printf 'max=%s half=%s current=%s actual=%s\n' $MAX $HALF "$(cat /sys/class/backlight/amdgpu_bl1/brightness)" "$(cat /sys/class/backlight/amdgpu_bl1/actual_brightness)"

echo "brightness to half: $HALF" > /sys/kernel/tracing/trace_marker
# stage3.sh

echo 0 > /sys/kernel/tracing/tracing_on
echo nop > /sys/kernel/tracing/current_tracer
sleep 1
kill $CATPID

分别执行上述stage1.shstage2.sh,亮度会变为50%。然后等一两秒时间在终端内执行:

echo "Fn brightness down" > /sys/kernel/tracing/trace_marker

执行后按下Fn亮度减,此时面板黑屏。
然后再按下Fn亮度加,并执行

echo "Fn brightness up" > /sys/kernel/tracing/trace_marker

完成后执行stage3.sh停止捕获。检查一下有没有丢事件:

sudo grep -i lost $OUT
sudo dmesg | grep -i mmiotrace

没有输出就是好事。如果丢了事件可以增加一下buffer大小,然后把开启捕获后的操作时间缩短一些。
然后把日志复制过来:

cp $OUT .

一般是复制到/root。重启进正常系统,然后用root去筛选一下日志就好了,或者自己cd一下要复制到的目录。

0x02

重启进正常系统后,执行下述Python脚本筛日志:

import sys

log_path = '/path/to/mmiotrace.log'
target = int('0xfd400138', 0)   # REG138
end = target + 4

with open(log_path, "r", errors="replace") as f:
    for line in f:
        fields = line.split()

        if not fields:
            continue

        if fields[0] == "MARK":   # 捕获时手动打的marker
            print(line, end="")
            continue

        if fields[0] not in {"R", "W"} or len(fields) < 7: # 只需要读写操作
            continue

        try:
            width = int(fields[1], 0)
            address = int(fields[4], 0)
            value = int(fields[5], 0)
        except ValueError:
            continue

        if address < end and address + width > target:  # 操作地址落在[target, target+4)
            extra = ""

            if width == 4 and address == target:
                bril = (value >> 8) & 0xff
                extra = f" decoded_BRIL=0x{bril:02x}"
                '''
                uint32_t r = readl(gpu_bar5 + 0x138);   
                uint8_t BRIL = (r >> 8) & 0xff; 
                '''

            print(line.rstrip() + extra)

根据Marker定位可以发现:

R 4 953.459481 32 0xfd400138 0x2000ff00 0x0 0 decoded_bril=0xff
W 4 953.459529 32 0xfd400138 0x20000000 0x0 0 decoded_bril=0x00

在通过sys class将亮度手动设置为一半后,在Linux内发生了向BRIL寄存器写入0x00的操作。
倒回去计算一下。这台设备的亮度值为0-65535,而在50%(32768)时:

50% = 32768 = 0x8000

(0x8000 << 8) & 0x0000ff00
= 0x00800000 & 0x0000ff00
= 0

换句话说,

BRIL = backlight_level & 0xff;

背光等级是一个16位字段,与0xFF进行位运算等同于取低8位。在50%时,0xFF00就被截断成0x00。在这台设备的ACPI DSDT中,BRIL字段应当就是屏幕亮度的意思,在亮度为0后再次按下亮度减会关闭面板显示,逻辑合情合理。

按此思路去想,在Linux内将亮度降为0%时,再按下亮度减的预期表现应当是关闭显示,但实际上并没有。为什么呢?
resource5+0x139 = 0x01 (1)
因为亮度为0%时实际亮度等级为1,哈哈。

目前的矛盾点已经明确:Linux(准确来说是amdgpu驱动)设置亮度时写入的是16位数据,而设备ACPI声明的数据长度是8位的。

OperationRegion (SCRA, SystemMemory, GBSA (), 0x04)
Field (SCRA, ByteAcc, NoLock, Preserve)
{
    Offset (0x01), 
    BRIL,   8
}

哪边是对的?
image.png

0x03

经过对amdgpu的一番大调查,发现其控制背光的代码如下:

// linux/drivers/gpu/drm/amd/amdgpu/amdgpu_atombios.c

void amdgpu_atombios_scratch_regs_set_backlight_level(
    struct amdgpu_device *adev,
    u32 backlight_level)
{
    u32 tmp = RREG32(adev->bios_scratch_reg_offset + 2);
    tmp &= ~ATOM_S2_CURRENT_BL_LEVEL_MASK;
    tmp |= (backlight_level << ATOM_S2_CURRENT_BL_LEVEL_SHIFT) &
           ATOM_S2_CURRENT_BL_LEVEL_MASK;
    WREG32(adev->bios_scratch_reg_offset + 2, tmp);
}

amdgpu使用寄存器号的形式访问寄存器,adev->bios_scratch_reg_offset + 2即表示amdgpu驱动中控制亮度的寄存器位于2号寄存器BIOS_SCRATCH_2
而在AMD AtomBios里,对BIOS_SCRATCH_2的定义是:

// linux/drivers/gpu/drm/amd/include/atombios.h

#define ATOM_S2_CURRENT_BL_LEVEL_MASK  0x0000FF00L
#define ATOM_S2_CURRENT_BL_LEVEL_SHIFT 8

// 并另外给出面向BIOS的按字节定义
#define ATOM_S2_CURRENT_BL_LEVEL_MASKb1 0xFF

0x0000FF00只覆盖bits 15:8,正好8 bit;右移8位,值域即为0x00-0xFF
但是在2025年6月,AMD向amdgpu驱动加入了“向用户空间导出完整亮度范围“改动。此前,amdgpu向用户空间暴露0x00-0xFF,但在此patch之后用户空间可见范围扩大到了面板/PWM完整范围,通常为0x0000-0xFFFF,以提供更高的亮度调节精度。

[WHY] Userspace currently is offered a range from 0-0xFF but the PWM
is programmed from 0-0xFFFF. This can be limiting to some software
that wants to apply greater granularity.

[HOW] Convert internally to firmware values only when mapping custom
brightness curves because these are in 0-0xFF range. Advertise full
PWM range to userspace.

但目前的amdgpu DM代码在这一更改后仍然执行

// linux/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c

dm->brightness[bl_idx] = user_brightness;

amdgpu_atombios_scratch_regs_set_backlight_level(
    dm->adev, dm->brightness[bl_idx]);

它在把亮度转换成实际面板控制值之前,就把完整的用户亮度原样传给了只有8位的ATOM scratch字段,没有经过缩放。
于是,0xFF00被截断为0x00,导致在当前amdgpu驱动下50%亮度时固件认为亮度已经到达0%,再按一次亮度减关闭了面板。
要解决这个问题,缩放一下应该就可以了。

scratch_level = user_brightness * 255 / user_max_brightness

比如,

static u8 amdgpu_dm_brightness_to_atombios_scratch(u32 brightness,
                            u32 max_brightness)
{
    const u32 scratch_max =
        ATOM_S2_CURRENT_BL_LEVEL_MASK >>
        ATOM_S2_CURRENT_BL_LEVEL_SHIFT;
    u32 level;

    if (!brightness || !max_brightness)
        return 0;

    brightness = min(brightness, max_brightness);

    level = DIV_ROUND_CLOSEST_ULL((u64)brightness * scratch_max,
                      max_brightness);

    return level;
}

然后

 static void amdgpu_dm_backlight_set_level(
     struct amdgpu_display_manager *dm,
     int bl_idx,
     u32 user_brightness)
 {
     struct amdgpu_dm_backlight_caps *caps;
     struct dc_link *link;
     u32 brightness;
     bool rc, reallow_idle = false;

     amdgpu_dm_update_backlight_caps(dm, bl_idx);
     caps = &dm->backlight_caps[bl_idx];

     dm->brightness[bl_idx] = user_brightness;

     /* update scratch register */
-    if (bl_idx == 0)
+    if (bl_idx == 0) {
+        struct backlight_device *bd = dm->backlight_dev[bl_idx];
+        u32 max_brightness = bd ? bd->props.max_brightness : AMDGPU_MAX_BL_LEVEL;
+        u8 scratch_level;
+
+        scratch_level =
+            amdgpu_dm_brightness_to_atombios_scratch(
+                user_brightness, max_brightness);
+
         amdgpu_atombios_scratch_regs_set_backlight_level(
-            dm->adev, dm->brightness[bl_idx]);
+            dm->adev, scratch_level);
+    }

     brightness = convert_brightness_from_user(
         caps, dm->brightness[bl_idx]);

上述补丁应该是有效的。但是编译Linux内核有点消耗资源,过几天再验证。

Copyright

原创 本文由 Elvin Starry 发布于 Est. Street

原文链接:https://www.frostleaf.dev/tech-research/why-my-embedded-screen-blacks-out-when-lowering-the-brightness-across-the-half-pt-2.html

许可协议CC BY-NC-SA 4.0署名-非商业性使用-相同方式共享 4.0 国际

发表评论

评论身份

保存后下次会自动带上,不用每次重新填写。